#!/bin/sh
# This script attempts to automatically install 32-bit compatibility libraries
# on 64-bit Linux systems. This addresses the issue where the 32-bit
# compatibility packages for some distributions (e.g. ia32-libs) don't provide
# all the 32-bit libraries needed for some applications (e.g. 64-bit Ubuntu
# Feisty doesn't provide a 32-bit libssl package).
#
# Copyright 2007 Google Inc. All Rights Reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
#   notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
#   copyright notice, this list of conditions and the following disclaimer
#   in the documentation and/or other materials provided with the
#   distribution.
# * Neither the name of Google Inc. nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

GUIMSG=true
CFGFILE="$HOME/.repackage32"
FAILURES=0

prompt_err() {
  echo "${DRYMSG}ERROR: $@"
  $GUIMSG -center -buttons "    OK    " -title "Error" "$@"
}

prompt_info() {
  echo "${DRYMSG}INFO: $@"
  $GUIMSG -center -buttons "    OK    " -title "Information" "$@"
}

prompt_yesno() {
  echo "$@"
  if [ "$DISPLAY" ]; then
    $GUIMSG -center -buttons "    Yes    ":0,"    No    ":1 -default "    Yes    " -title "Warning" "$@"
    return $? # 0-Yes, 1-No
  fi
  while [ "$YNREAD" != "n" ] &&
        [ "$YNREAD" != "N" ] &&
        [ "$YNREAD" != "y" ] &&
        [ "$YNREAD" != "Y" ]; do
    read -p "Select (Y)es/(N)o?: " YNREAD
    if [ "$YNREAD" = "y" ] || [ "$YNREAD" = "Y" ]; then
      return 0
    elif [ "$YNREAD" = "n" ] || [ "$YNREAD" = "N" ]; then
      return 1
    fi
  done
}

prompt_yesno_never() {
  echo "$@"
  if [ "$DISPLAY" ]; then
    $GUIMSG -center -buttons "    Yes    ":0,"    No    ":1," No don't ask again ":2 \
    -default "    Yes    " -title "Warning" "$@"
    return $? # 0-Yes, 1-No
  fi
  while [ "$YNREAD" != "n" ] &&
        [ "$YNREAD" != "N" ] &&
        [ "$YNREAD" != "d" ] &&
        [ "$YNREAD" != "D" ] &&
        [ "$YNREAD" != "y" ] &&
        [ "$YNREAD" != "Y" ]; do
    read -p "Select (Y)es/(N)o/(D)on't ask again?: " YNREAD
    if [ "$YNREAD" = "y" ] || [ "$YNREAD" = "Y" ]; then
      return 0
    elif [ "$YNREAD" = "n" ] || [ "$YNREAD" = "N" ]; then
      return 1
    elif [ "$YNREAD" = "d" ] || [ "$YNREAD" = "D" ]; then
      return 2
    fi
  done
}

close_progress() {
  if [ $PROGRESSPID ]; then
    kill $PROGRESSPID > /dev/null 2>&1
  fi
  unset PROGRESSPID
}

prompt_progress() {
  close_progress
  echo "$@"
  $GUIMSG -center -buttons "" -title "Working..." "$@" &
  PROGRESSPID=$!
}

usage() {
  SEP="-------------------------------------------------------"
  prompt_info "
$SEP
This program attempts to install 32-bit compatibility libraries on
64-bit Linux systems. For instance, to install the 32-bit OpenSSL
libraries, you would run:

  $(basename $0) openssl

This would allow your 64-bit system to run 32-bit programs which
require OpenSSL. See below for a complete list of program options
and supported compatibility packages.
$SEP
Usage: $(basename $0) [-t|-n|-f] [-p message] [-c] <package ...>
  -t  Test if <package> is already installed.
  -n  Don't actually install, just show what would happen.
  -f  Force <package> install, even if already installed.
  -p  Prompt to display before installing <package>.
  -c  Create the <package>, but don't install.
  -h  Show this help message.
$SEP
Packages:
  openssl   OpenSSL libraries
$SEP"
}

trap cleanup HUP INT QUIT ABRT TERM EXIT
cleanup() {
  set +ex
  close_progress
}

if [ "`uname -m`" != "x86_64" ] ; then
  prompt_err "$(basename $0) is intended only for x86_64 systems."
  exit 1
fi

prep_fetch() {
  if [ "$FETCHPROG" ]; then
    return 0;
  fi

  FETCHCOMMANDS="curl wget w3m links"
  for FETCHPROG in $FETCHCOMMANDS; do
    if [ "$(which $FETCHPROG 2>/dev/null)" ]; then
      break
    fi
  done

  if [ ! "$FETCHPROG" ] && [ ! $TESTMODE ]; then
    prompt_err "Couldn't find a program to use to download files.
  Please install one of the following programs:
  $FETCHCOMMANDS"
    return 1
  fi
}

do_fetch() {
  URL="$1"
  SAVEFILE="$2"
  prompt_progress "Fetching '$URL'."

  # Don't actually download anything for a dry run.
  if [ $DRYRUN ]; then
    return 0
  fi

  case $FETCHPROG in
    curl )
      curl -s "$URL" > "$SAVEFILE"
      ;;
    wget )
      wget -q -O "$SAVEFILE" "$URL"
      ;;
    w3m )
      w3m -dump_source "$URL" > "$SAVEFILE"
      ;;
    links )
      links -source "$URL" > "$SAVEFILE"
      ;;
    * )
      prompt_err "Unknown fetch command '$FETCHPROG'."
      return 1
      ;;
  esac
}

run_as_root() {
  PKGNAME=$1
  shift
  PKGCMD=$1
  shift
  local ARGS=""
  while [ $# -gt 0 ]; do
    ARGS="$ARGS\"$1\" "
    shift
  done

  if [ `id -u` != '0' ]; then
    GETROOTMSG="$PKGNAME installation requires root privileges.
"

    # Only run GUI tools if we're in X.
    if [ "$DISPLAY" ]; then
      ROOTPRG=$(which gksu 2>/dev/null)
      if [ "$ROOTPRG" ]; then
        prompt_progress "$GETROOTMSG Trying 'gksu'. Enter your password if prompted."
        # NOTE - gksu is supposed to return non-zero if it fails or is cancelled,
        # but it seems to always return 0 unless the password is wrong, so we
        # have to jump through some hoops to see if the command actually runs.
        # We'll put the command in a temp file that deletes itself on run.
        # If the temp file is gone, we'll assume it ran.
        GKSUSTAT=$(mktemp)
        echo "$PKGCMD $ARGS" >> $GKSUSTAT
        echo "rm '$GKSUSTAT'" >> $GKSUSTAT
        prompt_progress "Running '$PKGCMD $ARGS'."
        $ROOTPRG sh $GKSUSTAT
        if [ $? -eq 0 ] && [ ! -f "$GKSUSTAT" ]; then
          return 0
        fi
      fi
    fi

    # Only run command-line tools if we're in a terminal.
    if [ "$TERM" ] && [ "$TERM" != "dumb" ]; then
      ROOTPRG=$(which sudo 2>/dev/null)
      if [ "$ROOTPRG" ]; then
        prompt_progress "$GETROOTMSG Trying 'sudo'. Enter your password if prompted."
        $ROOTPRG $PKGCMD $ARGS
        if [ $? -eq 0 ]; then
          return 0
        fi
      fi

      prompt_progress "$GETROOTMSG Trying 'su'. Enter root's password when prompted."
      su root -c "$PKGCMD $ARGS"
      if [ $? -eq 0 ]; then
        return 0
      fi
    fi

    echo
    prompt_err "Could not run '$PKGCMD' as root.
Please re-run from a root login."
    return 1
  else
    $PKGCMD $ARGS
  fi
}

guess_distro() {
  if [ "$DISTRO" ]; then
    return 0
  fi

  if [ -f /etc/lsb-release ]; then
    eval $(sed -e '/DISTRIB_ID/!d' /etc/lsb-release)
    if [ "$DISTRIB_ID" ]; then
      case $DISTRIB_ID in
      *buntu)
        DISTRO=Ubuntu
        ;;
      Mandriva*)
        DISTRO=Mandriva
        ;;
      * )
        # TODO Update as necessary to canonicalize values used by other distros.
        DISTRO="$DISTRIB_ID"
        ;;
      esac
      return 0
    fi
  fi

  if [ -f /etc/debian_release ] || [ -f /etc/debian_version ]; then
    DISTRO=Debian
    return 0
  fi

  if [ -f /etc/fedora-release ]; then
    DISTRO=Fedora
    return 0
  fi

  if [ -f /etc/SuSE-release ]; then
    DISTRO=Suse
    return 0
  fi

  if [ -f /etc/mandriva-release ]; then
    DISTRO=Mandriva
    return 0
  fi

  if [ -f /etc/redhat-release ] || [ -f /etc/redhat_version ]; then
    DISTRO=Redhat
    return 0
  fi

  return 1
}

fetch_deb() {
  DISTROPKG=$1
  SAVEFILE="$2"

  DPKGDIR="$PWD/dpkg.dummy"
  mkdir -p "$DPKGDIR/partial"

  # Figure out where to fetch the package from.
  # Create a modified dpkg status file to make it look like libssl is not
  # installed yet, otherwise --print-uris won't output anything.
  DPKGDUMMYSTAT="$DPKGDIR/status"
  DPKGSTATUS=$(apt-config dump | sed -e '/Dir::State::status/!d' \
    -e "s/Dir::State::status \"\(.*\)\".*/\1/")
  sed -e "s/Package: $DISTROPKG/Package: $DISTROPKG.orig/" "$DPKGSTATUS" \
    > "$DPKGDUMMYSTAT"
  DEBURL=$(apt-get -o Dir::Cache::Archives="$DPKGDIR" \
    -o Dir::State::status="$DPKGDUMMYSTAT" --print-uris install $DISTROPKG | \
    grep $DISTROPKG | tail -n 1 | sed -e "s/'\(.*\.deb\)'.*/\1/" -e "s/_amd64/_i386/")

  rm -rf "$DPKGDIR"

  # Fetch and unpack the deb to reformat it as a 64-bit compatibility package.
  do_fetch "$DEBURL" "$SAVEFILE"
  if [ ! -f "$SAVEFILE" ] && [ ! $DRYRUN ]; then
    prompt_err "Failed to download $DISTROPKG package from
$DEBURL"
    return 1
  fi
}

guess_latest_openssl_deb() {
  apt-cache search --names-only "libssl[^-]+$" | \
    sed -e '/libssl[[:digit:]\.]\+.*/!d' \
        -e "s/libssl\([[:digit:]\.]\+\).*/\1/" | \
    sort | tail -n 1
}

guess_latest_openssl() {
  # Try to determine the latest libssl version available for this distro.
  guess_distro
  case $DISTRO in
    Debian )
      guess_latest_openssl_deb
      ;;
    Ubuntu )
      guess_latest_openssl_deb
      ;;
    * )
      echo "$BASEVER"
      ;;
  esac
}

init_openssl() {
  BASEVER=0.9.8
  if [ ! "$CURRVER" ]; then
    CURRVER=$(guess_latest_openssl)
  fi
}

test_openssl() {
  init_openssl
  # TODO - Do we want to test for other versions? Whatever we test for should
  # match what we attempt to install with fetch_deb.
  SSLCHECK=$(ldconfig -p | grep -v "," | \
    grep "libssl.so.\($BASEVER\|$CURRVER\)")
  SSLRES=$?
  CRYPTOCHECK=$(ldconfig -p | grep -v "," | \
    grep "libcrypto.so.\($BASEVER\|$CURRVER\)")
  CRYPTORES=$?
  if [ $SSLRES -eq 0 ] && [ $CRYPTORES -eq 0 ]; then
    # Don't let this pester the user with a popup.
    GUIMSG=true
    prompt_info "The openssl libs are already installed.
$SSLCHECK
$CRYPTOCHECK"
    return 0
  else
    return 1
  fi
}

repackage_openssl_deb() {
  SAVEFILE="$1"

  if [ $DRYRUN ]; then
    return 0
  fi

  # Sanity check to make sure the deb conatins the files we ultimately want, so
  # it will pass the test_openssl tests.
  dpkg -c "$SAVEFILE" | grep -qs "libssl.so.\($BASEVER\|$CURRVER\)"
  SSLRES=$?
  dpkg -c "$SAVEFILE" | grep -qs "libcrypto.so.\($BASEVER\|$CURRVER\)"
  CRYPTORES=$?
  if [ $SSLRES -ne 0 ] || [ $CRYPTORES -ne 0 ]; then
    prompt_err "Don't know how to repackage $SAVEFILE."
    return 1
  fi

  ar xo "$SAVEFILE"
  tar -zxf data.tar.gz
  rm data.tar.gz
  tar -zxf control.tar.gz
  rm control.tar.gz

  # Put the libs in the 32-bit hierarchy
  mv usr/lib usr/lib32
  sed -i -e "s/usr\/lib\//usr\/lib32\//" md5sums

  # Remove unused files
  rm -rf usr/share
  sed -i -e "/usr\/share\//d" md5sums
  rm shlibs templates

  # Rewrite package files
  sed -i -e "s/\(Architecture:\).*/\1 amd64/" control
  sed -i -e "s/\(Package:\).*/\1 lib32ssl0.9.8/" control
  # Ignore dependencies and conflicts since the compatibility packages don't
  # necessarily conincide with the regular i386 packages.
  # TODO Maybe we can rewrite these rather than just ignoring them.
  sed -i -e "/^Depends:/d" control
  sed -i -e "/^Conflicts:/d" control

  # Rebuild the deb
  tar -czf data.tar.gz ./usr
  tar -czf control.tar.gz --exclude data.tar.gz --exclude "$SAVEFILE" \
    --exclude control.tar.gz --exclude usr .
  ar r "$SAVEFILE" control.tar.gz data.tar.gz
}

install_openssl_deb() {
  prompt_progress "Installing openssl libs."
  DISTROPKG=libssl0.9.8
  COMPATPKG=lib32ssl0.9.8
  TOPDIR="$PWD"
  WORKDIR=$(mktemp -d)
  if [ $? -gt 0 ]; then
    prompt_err "Couldn't create working directory."
    return 1
  fi
  cd "$WORKDIR"

  SAVEFILE="$WORKDIR/$COMPATPKG.deb"
  fetch_deb "$DISTROPKG" "$SAVEFILE"
  if [ $? -ne 0 ]; then
    return 1
  fi

  prompt_progress "Creating $COMPATPKG package."
  mkdir -p "$WORKDIR/$DISTROPKG"
  cd "$WORKDIR/$DISTROPKG"
  repackage_openssl_deb "$SAVEFILE"
  if [ $? -ne 0 ]; then
    return 1
  fi
  cd "$TOPDIR"
  rm -rf "$WORKDIR/$DISTROPKG"

  if [ $CREATEONLY ]; then
    prompt_info "openssl package saved as:
'$SAVEFILE'"
  else
    prompt_progress "Installing $COMPATPKG package.
Enter your administrative password if prompted."
    if [ ! $DRYRUN ]; then
      run_as_root openssl "dpkg -i $SAVEFILE"
      if [ $? -ne 0 ]; then
        return 1
      fi
    fi
    # cleanup
    rm -rf "$WORKDIR"
  fi
}

install_openssl() {
  init_openssl
  guess_distro
  case $DISTRO in
    Debian )
      install_openssl_deb
      ;;
    Ubuntu )
      install_openssl_deb
      ;;
    * )
      prompt_err "Don't know how to install openssl on $DISTRO."
      return 1
      ;;
  esac
  return $?
}

test_packages() {
  # When in Test-only mode, we return from here with these codes:
  # 0 - package already installed
  # 1 - package not found, but can be installed
  # 2 - package not found, and don't know how to install.
  # Note: Test-only mode only makes sense for one package at a time.
  # When not in test-only mode, returns the number of packages not found.
  NOTFOUND=0
  if [ $RUN_OPENSSL -gt 0 ]; then
    test_openssl
    TESTRES=$?
    if [ $TESTRES -eq 0 ]; then
      RUN_OPENSSL=0
    else
      NOTFOUND=$(($NOTFOUND+ 1))
    fi
    if [ $TESTMODE ]; then
      if [ $TESTRES -eq 0 ]; then
        exit 0
      fi
      "$0" -n openssl
      if [ $? -eq 0 ]; then
        exit 1
      else
        exit 2
      fi
    fi
  fi
  return $NOTFOUND
}

run_packages() {
  if [ $RUN_OPENSSL -gt 0 ]; then
    is_supressed openssl
    if [ $? -ne 0 ]; then
      install_openssl
      if [ $? -ne 0 ]; then
        fail openssl
      fi
    fi
  fi
}

parse_packages() {
  RUN_OPENSSL=0
  BADPKGS=""

  if [ $# -eq 0 ]; then
    prompt_err "Please specify at least one package to install."
    return 1
  fi

  while [ $# -gt 0 ]; do
    case $1 in
      openssl )
        RUN_OPENSSL=1
        ;;
      * )
        BADPKGS="$BADPKGS $1"
        ;;
    esac
    shift
  done

  if [ "$BADPKGS" ]; then
    for pkg in $BADPKGS; do
      prompt_err "invalid package name '$pkg'"
    done
    return 1
  fi
}

is_supressed() {
  PACKAGE=$1
  grep -qs "$PACKAGE 1 $(hostname -f)" "$CFGFILE"
  return $?
}

fail() {
  FAILURES=$(($FAILURES + 1))
  PACKAGE=$1
  # Call without an exit code to continue processing other packages.
  EXITCODE=$2
  prompt_err "Installation of $PACKAGE failed."
  if [ "$EXITCODE" ]; then
    exit $EXITCODE
  fi
}

#=========
# MAIN
#=========
while getopts ":tnfcp:h" OPTNAME
do
  case $OPTNAME in
    t )
      TESTMODE=1
      ;;
    n )
      DRYRUN=1
      DRYMSG="DRY RUN "
      ;;
    p )
      PROMPTMSG="$OPTARG"
      ;;
    f )
      FORCE=1
      ;;
    c )
      CREATEONLY=1
      ;;
    h )
      usage
      exit 0
      ;;
    \: )
      prompt_err "'-$OPTARG' needs an argument."
      usage
      exit 1
      ;;
    * )
      prompt_err "invalid command-line option: $OPTARG"
      usage
      exit 1
      ;;
  esac
done
shift $(($OPTIND - 1))

# Scan remaining args for packages to run.
parse_packages "$@"
if [ $? -gt 0 ]; then
  usage
  exit 1
fi

# Now that we're past general usage errors, we'll allow GUI output.
# But not in test or dry run mode.
if [ ! $TESTMODE ] && [ ! $DRYRUN ]; then
  if [ "$DISPLAY" ]; then
    type xmessage > /dev/null 2>&1 && GUIMSG=xmessage
    type gmessage > /dev/null 2>&1 && GUIMSG=gmessage
    type kmessage > /dev/null 2>&1 && GUIMSG=kmessage
  fi
fi

# No point going farther if we don't have a way to download the files we need.
prep_fetch
if [ $? -gt 0 ]; then
  exit 1
fi

# Test if packages are already installed before we try (unless forced).
if [ ! $FORCE ]; then
  test_packages
  # If no packages remain to install, don't bother with the prompt.
  if [ $? -eq 0 ]; then
    exit 0
  fi
fi

if [ "$PROMPTMSG" ]; then
  PACKAGE=$(echo "$PROMPTMSG" | md5sum | cut -d ' ' -f 1)
  is_supressed "$PACKAGE"
  if [ $? -eq 0 ]; then
    exit 0
  fi
  prompt_yesno_never "$PROMPTMSG"
  RES=$?
  if [ $RES -gt 0 ]; then
    if [ $RES -eq 2 ]; then
      # Similar to fail(), but will avoid retrying this specific, entire
      # request again, rather than individual packages.
      echo "$PACKAGE 1 $(hostname -f)" >> "$CFGFILE"
    fi
    exit 1
  fi
fi

# Now actually install any packages which weren't ruled out by the tests.
run_packages

exit $FAILURES
